package sgf;

import java.awt.Point;
import java.awt.Rectangle;
import java.util.*;

import ui.Main;

import board.Board;
import board.NodeLabel;
import board.NodeMark;
import board.SimpleMark;
import board.command.AddCommand;
import board.command.Command;
import board.command.CompositeCommand;
import board.command.MoveCommand;

/**
 * Represents a node within an SGF file.
 * @author TKington
 *
 */
public class SGFNode {
	/** The parent node. */
    protected SGFNode parent;
    /** This node's children. */
    protected ArrayList<SGFNode> ch;
    /** Commands that should be executed when navigating to this node. */
    protected ArrayList<Command> cmds;
    /** SGF properties associated with this node. */
    protected HashMap<String, ArrayList<String>> props;
    
    /** Board size. */
    protected int size;
    /** Node comment. */
    protected String comment;
    /** Marks to be displayed on board. */
    protected ArrayList<NodeMark> marks;
    /** Is this the end of the correct variation? */
	private boolean right;
	/** Is this node on the path to a correct answer? */
	private boolean rightPath;
	/** Was this node created from the SGF file? */
	protected boolean fromFile;
	/** This object represents a move at the point pos. */
	protected Point pos;
	/** Player who plays this move. */
	protected int player;
    
	/**
	 * Creates an SGFNode.
	 * @param parent the parent node
	 */
    private SGFNode(SGFNode parent) {
        this.parent = parent;
        ch = new ArrayList<SGFNode>();
        cmds = new ArrayList<Command>();
        props = new HashMap<String, ArrayList<String>>();
        marks = new ArrayList<NodeMark>();
    }
    
    /**
     * Creates an SGFNode representing a move on the Board.
     * @param parent the parent SGFNode
     * @param x the x location
     * @param y the y location
     */
    public SGFNode(SGFNode parent, int x, int y) {
        this(parent);
        fromFile = false;
        pos = new Point(x,y);
        player = parent.player * -1;
        cmds.add(new MoveCommand(player, pos));
        comment = ""; //$NON-NLS-1$
    }
    
    /**
     * Creates an SGFNode.
     * @param sgfData an SGF representation of the node
     * @param parent the parent SGFNode
     * @throws SGFParseException if the String can't be parsed successfully
     */
    public SGFNode(String sgfData, SGFNode parent) throws SGFParseException {
        this(parent);
        fromFile = true;
        
        parse(sgfData);
    }
    
    /**
     * Populates this SGFNode with the data in the passed SGF string.
     * @param sgfData the SGF description of the node
     * @throws SGFParseException if the SGF string is illegal
     */
    protected void parse(String sgfData) throws SGFParseException {
    	cmds.clear();
    	props.clear();
    	marks.clear();
    	comment = null;
    	size = 0;
    	
		try {
            SGFParser.parseTags(sgfData, this);
        }
        catch(Exception e) {
            throw new SGFParseException(e.getMessage() + " sgf=" + sgfData); //$NON-NLS-1$
        }
        
        String sz = getProperty("SZ"); //$NON-NLS-1$
        if(sz != null)
            size = Integer.parseInt(sz);
        Iterator iter = props.keySet().iterator();
	    while(iter.hasNext()) {
	        String tag = (String)iter.next();
	        ArrayList propVals = props.get(tag);
	        
	        if(tag.equals("B")) { //$NON-NLS-1$
	            player = 1;
	            pos = SGFUtils.stringToPoint((String)propVals.get(0));
	        }
	        else if(tag.equals("W")) { //$NON-NLS-1$
	            player = -1;
	            pos = SGFUtils.stringToPoint((String)propVals.get(0));
	        }
	        else if(tag.equals("AE")) { //$NON-NLS-1$
	        	for(int i = 0; i < propVals.size(); i++) {
	        		Point p = SGFUtils.stringToPoint((String)propVals.get(i));
	        		for(int j = 0; j < cmds.size(); j++) {
	        			Command c = cmds.get(j);
	        			if(c instanceof AddCommand) {
	        				AddCommand a = (AddCommand)c;
	        				a.removePoint(p);
	        			}
	        		}
	        	}
	        }
	    }
	    
	    if(comment != null) {
	        right |= stripFromComment("RIGHT"); //$NON-NLS-1$
	        right |= stripFromComment("CORRECT"); //$NON-NLS-1$
	        stripFromComment("CHOICE"); //$NON-NLS-1$
	    }
	}

    /**
     * Returns the list of SGF properties of a given type at this node. 
     * @param tag the SGF tag type
     * @return the list of properties
     */
	public ArrayList<String> getPropertyList(String tag) {
        ArrayList<String> l = props.get(tag);
        if(l == null) {
            l = new ArrayList<String>();
            props.put(tag, l);
        }
        /*else {
            if(!tag.equals("XS") && !tag.equals("YG") && !tag.equals("B") && //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            !tag.equals("W") && !tag.equals("YB") && !tag.equals("YW") && //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            !tag.equals("LB") && !tag.equals("XB") && !tag.equals("XW") && //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            !tag.equals("LS") && !tag.equals("LN") && !tag.equals("TW") && //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            !tag.equals("TB") && !tag.equals("XG") && !tag.equals("LR")) //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                printProp(tag); //System.out.println("Mult: " + tag);
        }*/
        
        return l;
    }
    
	/**
	 * Adds an SGF tag and properties to the current node.
	 * @param tag the SGF tag
	 * @param propVals the properties
	 * @throws SGFParseException if the properties are illegal
	 */
    public void addTag(String tag, ArrayList<String> propVals) throws SGFParseException {
        if(tag.equals("C")) { //$NON-NLS-1$
            comment = propVals.get(0);
        }
        else if(!NodeMark.create(marks, tag, propVals)) {
            Command cmd = Command.createCommand(tag, propVals);
            if(cmd != null)
                cmds.add(cmd);
        }
        
        getPropertyList(tag).addAll(propVals);
    }
    
    /**
     * Executes this SGFNode on the given Board.
     * @param board the Board
     */
    public void execute(Board board) {
        board.setMarks(marks);        
        CompositeCommand c = new CompositeCommand(cmds);
        board.executeCommand(c);
    }
    
    /**
     * Updates the Board's state with the information in this SGFNode.
     * @param b
     */
    public void updateState(Board b) {
    	b.setMarks(marks);
	    ArrayList<Point> good = new ArrayList<Point>();
	    ArrayList<Point> bad = new ArrayList<Point>();
	    getNavPts(good, bad);
	    b.setGoodMoves(good);
	    b.setBadMoves(bad);
    }
    
    /**
     * Checks if this SGFNode has a specific AddCommand.
     * @param x the x location
     * @param y the y location
     * @param color 1 for black, -1 for white
     * @return true if the AddCommand is found
     */
    public boolean hasAddCommand(int x, int y, int color) {
    	for (Iterator iter = cmds.iterator(); iter.hasNext();) {
			Object o = iter.next();
			if(o instanceof AddCommand) {
				AddCommand c = (AddCommand)o;
				if(c.getPlayer() == color &&
						c.hasPoint(x, y))
					return true;
			}
		}
    	
    	return false;
    }
    
    /**
     * Removes a specific AddCommand from this SGFNode.
     * @param x the x location
     * @param y the y location
     * @param color 1 for black, -1 for white
     */
    public void removeAddCommand(int x, int y, int color) {
    	for (Iterator iter = cmds.iterator(); iter.hasNext();) {
			Object o = iter.next();
			if(o instanceof AddCommand) {
				AddCommand c = (AddCommand)o;
				if(c.getPlayer() == color &&
						c.removePoint(x, y))
					return;
			}
		}
    }
    
    /**
     * Creates an AddCommand at this SGFNode.
     * @param x the x location
     * @param y the y location
     * @param color 1 for black, -1 for white
     */
    public void addAddCommand(int x, int y, int color) {
    	AddCommand c = null;
    	for (Iterator iter = cmds.iterator(); iter.hasNext();) {
			Object o = iter.next();
			if(o instanceof AddCommand) {
				AddCommand a = (AddCommand)o;
				if(a.getPlayer() == color) {
					c = a;
					break;
				}
			}
		}
    	
    	if(c == null) {
    		c = new AddCommand(color);
    		cmds.add(c);
    	}
    	
    	c.addPoint(new Point(x, y));
    }
    
    /**
     * Checks to see if this SGFNode contains a given Command.
     * @param c the Command
     * @return true if the Command is found
     */
    public boolean hasCommand(Command c) {
    	return cmds.contains(c);
    }
    
    /**
     * Adds a Command to this SGFNode.
     * @param c the Command
     */
    public void addCommand(Command c) {
    	cmds.add(c);
    }
    
    /**
     * Removes a Command from this SGFNode.
     * @param c the Command
     * @return true if the Command was found and removed
     */
    public boolean removeCommand(Command c) { 
    	return cmds.remove(c);
    }
    
    /**
     * Gets the properties for a given SGF tag.
     * @param tag the SGF tag
     * @return the associated properties
     */
    public String getProperty(String tag) {
        ArrayList<String> l = props.get(tag);
        if(l == null)
            return null;
        
        return l.get(0);
    }
    
    /**
     * Adds a child SGFNode.
     * @param n the child
     */
    public void addChild(SGFNode n) {
        ch.add(n);
    }
    
    /**
     * Checks to make sure this SGFNode is valid on a Board of a given size.
     * @param boardSize the Board size
     * @throws SGFParseException if the SGFNode is invalid
     */
    public void validatePoints(int boardSize) throws SGFParseException {
    	if(pos != null)
	        SGFUtils.checkPoint(pos, boardSize);
	    
	    for(int i = 0; i < marks.size(); i++) {
            NodeMark m = marks.get(i);
            m.validatePoints(boardSize);
        }
    
        for(int i = 0; i < cmds.size(); i++) {
            Command c = cmds.get(i);
            c.validatePoints(boardSize);
        }
    
        for(int i = 0; i < ch.size(); i++) {
            SGFNode n = ch.get(i);
            n.validatePoints(boardSize);
        }
    }
    
    /**
     * Prints the properties associated with a specific SGF tag
     * @param tag the SGF tag
     */
    public void printProp(String tag) {
        System.out.println(tag);
        ArrayList<String> p = props.get(tag);
        if(p == null) {
            System.out.println(":null"); //$NON-NLS-1$
            return;
        }
        
        for(int i = 0; i < p.size(); i++) {
            System.out.println("[" + p.get(i) + "]"); //$NON-NLS-1$ //$NON-NLS-2$
        }
    }
    
    /**
     * Returns the SimpleMark of a given type at a given position.
     * @param x the x location
     * @param y the y location
     * @param type the type
     * @return the SimpleMark, or null if not found
     */
    public SimpleMark getSimpleMark(int x, int y, int type) {
        for(int i = 0; i < marks.size(); i++) {
            Object o = marks.get(i);
            if(o instanceof SimpleMark) {
            	SimpleMark m = (SimpleMark)o;
	            Point p = m.getPoint();
	            if(p.x == x && p.y == y && m.getType() == type) {
	                return m;
	            }
            }
        }
        
        return null;
    }
    
    /**
     * Gets the first SimpleMark found at a given x,y location.
     * @param p the x,y location
     * @return the SimpleMark
     */
	protected SimpleMark getMarkAt(Point p) {
	    for(int j = 0; j < marks.size(); j++) {
	        NodeMark m = marks.get(j);
	        if(m instanceof SimpleMark) {
	        	SimpleMark s = (SimpleMark)m;
	        	if(p.equals(s.getPoint()))
	        		return (SimpleMark)m;
	        }
	    }
	    return null;
	}
    
	/**
	 * Returns the next alphabetic label that is not in use at this node.
	 * @return the label
	 */
    public String getNextLabel() {
    	for(int j = -1; j < 26; j++) {
    		String prefix = ""; //$NON-NLS-1$
    		if(j != -1) {
    			char c = (char)('A' + j);
    			prefix += c;
    		}
    		
	    	for(int i = 0; i < 26; i++) {
	    		char c = (char)('A' + i);
	    		String l = prefix + c;
	    		
	    		boolean found = false;
	    		for (Iterator iter = marks.iterator(); iter.hasNext();) {
					Object o = iter.next();
					if(o instanceof NodeLabel) {
						NodeLabel label = (NodeLabel)o;
						if(l.equals(label.getText())) {
							found = true;
							break;
						}
					}
				}
	    		
	    		if(!found)
	    			return l;
	    	}
    	}
    	
    	return "You must be joking"; //$NON-NLS-1$
    }
    
    /**
     * Returns the smallest number that is not already in use at this node.
     * @return the number
     */
    public String getNextNumberLabel() {
    	for(int i = 1; i < 500; i++) {
    		String l = String.valueOf(i);
    		
    		boolean found = false;
    		for (Iterator iter = marks.iterator(); iter.hasNext();) {
				Object o = iter.next();
				if(o instanceof NodeLabel) {
					NodeLabel label = (NodeLabel)o;
					if(l.equals(label.getText())) {
						found = true;
						break;
					}
				}
			}
    		
    		if(!found)
    			return l;
    	}
    	
    	return null;
    }
    
    /**
     * Adds a mark to this SGFNode
     * @param m the NodeMark to be added
     */
    public void addMark(NodeMark m) {
    	marks.add(m);
    	if(m instanceof SimpleMark) {
    		SimpleMark s = (SimpleMark)m;
    		if(s.getType() < 4)
    			Collections.sort(marks);
    	}
    }
    
    /**
     * Removes a mark from this SGFNode.
     * @param m the NodeMark to be removed
     */
    public void removeMark(NodeMark m) { marks.remove(m); }

    /**
     * Strips the given string off of the beginning of the comment.
     * @param s the string to remove
     * @return true if the string was found
     */
	private boolean stripFromComment(String s) {
	    int index = comment.indexOf(s);
	    if(index == -1)
	        return false;
	    
	    while(index != -1) {
	        String beg = comment.substring(0, index);
	        String end = comment.substring(index + s.length());
	        if(index == 0)
	            end = end.trim();
	        
	        comment = beg + end;
	        index = comment.indexOf(s);
	    }
	    
	    return true;
	}

	/**
	 * Marks paths right/wrong based on comments starting with "RIGHT".  Paths
	 * without such comments are marked as wrong.
	 * @return true if at least one such comment was found
	 */
	public boolean markPathsRIGHT() {
	    boolean ret = false;
	    
	    if(right) {
	        SGFNode n = this;
	        while(n != null) {
	            n.rightPath = true;
	            n = n.parent;
	        }
	        ret = true;
	    }
	    
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        ret |= n.markPathsRIGHT();
	    }
	    
	    return ret;
	}

	/**
	 * Marks paths right/wrong based on WV tags.  Paths are assumed to be correct
	 * unless they are marked. 
	 *
	 */
	public void markPathsWV() {
	    if(parent == null) {
	        rightPath = true;
	    }
	    else if(parent.rightPath) {
	        if(props.get("WV") == null) { //$NON-NLS-1$
	            boolean foundTR = false;
	            for(int i = 0; i < marks.size(); i++) {
	                NodeMark m = marks.get(i);
	                if(m.getType() == NodeMark.TRI && pos != null 
	                            && pos.equals(((SimpleMark)m).getPoint())) {
	                        marks.remove(i);
	                        foundTR = true;
	                        break;
	                }
	            }
	            
	            if(!foundTR) {
	                rightPath = true;
	                right = ch.isEmpty();
	            }
	        }
	    }
	    
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        n.markPathsWV();
	    }
	}

	/**
	 * Returns the bounding Rectangle that is affected by this SGFNode.
	 * @return the bounding Rectangle
	 */
	public Rectangle getBounds() {
	    Rectangle r = null;
	    for(int i = 0; i < cmds.size(); i++) {
	        Command c = cmds.get(i);
	        if(r == null)
	            r = c.getBounds();
	        else r = r.union(c.getBounds());
	    }
	    
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        Rectangle b = n.getBounds();
	        if(b != null) {
	            if(r == null)
	                r = n.getBounds();
	            else r = r.union(b);
	        }
	    }
	    
	    for(int i = 0; i < marks.size(); i++) {
	        NodeMark m = marks.get(i);
	        if(r == null)
	        	r = m.getBounds();
	        else r = r.union(m.getBounds());
	    }
	    
	    return r;
	}

	/**
	 * Fills the lists with good and bad follow-up moves. 
	 * @param good the list to be filled with good moves
	 * @param bad the list to be filled with bad moves
	 */
	public void getNavPts(ArrayList<Point> good, ArrayList<Point> bad) {
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        if(n.pos != null) {
	            if(n.fromFile) {
	                if(n.rightPath)
	                    good.add(n.pos);
	                else bad.add(n.pos);
	            }
	        }
	    }
	}

	/**
	 * Returns the player who should make the next move.
	 * @return 1 for black, -1 for white
	 */
	public int getNextPlayer() {
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        if(n.pos != null)
	            return n.player;
	    }
	    return 1;
	}

	/**
	 * Returns the SGFNode representing a follow-up at the given Point.
	 * @param p the Point
	 * @return the SGFNode, or null if there is no follow-up at that Point
	 */
	public SGFNode getNextNode(Point p) {
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        if(n.pos != null && n.pos.equals(p))
	            return n;
	    }
	    return null;
	}

	/**
	 * Randomly chooses a response from the follow-up moves in the SGF file.
	 * @return the SGFNode representing the response
	 */
	public SGFNode getResponse() {
	    int num = 0;
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        //  Don't leave the right path when responding
	        //  Many problems from goproblems.com have this potential
	        //  Also don't show nodes that were added by user in navsoln
	        if(n.pos != null && n.fromFile && (n.rightPath || !rightPath))
	            num++;
	    }
	    
	    if(num == 0)
	        return null;
	    
	    int r = Main.rand.nextInt(num);
	    
	    num = 0;
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        if(n.pos != null && (n.rightPath || !rightPath)) {
	            if(num == r)
	                return n;
	            num++;
	        }
	    }
	    
	    throw new RuntimeException();
	}

	/**
	 * Returns true if this SGFNode has follow-up moves.
	 * @return true if this SGFNode has follow-up moves
	 */
	public boolean hasChildMoves() {
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        if(n.pos != null)
	            return true;
	    }
	    return false;
	}

	/**
	 * Returns true if this SGFNode has follow-up moves that were in the SGF file.
	 * @return true if this SGFNode has follow-up moves that were in the SGF file
	 */
	public boolean hasChildrenFromFile() {
	    for(int i = 0; i < ch.size(); i++) {
	        SGFNode n = ch.get(i);
	        if(n.fromFile)
	            return true;
	    }
	    return false;
	}

	/**
	 * Returns true if this SGFNode is a correct answer.
	 * @return true if this SGFNode is a correct answer
	 */
	public boolean isRight() { return right; }
	
	/**
	 * Checks if this SGFNode is on the path to a correct answer.
	 * @return true if this SGFNode is on the correct path
	 */
	public boolean isOnRightPath() { return rightPath; }
	
	/**
	 * Returns the current player at this SGFNode.
	 * @return 1 for black, -1 for white
	 */
	public int getPlayer() { return player; }
	
	/**
	 * Sets the current player at this SGFNode.
	 * @param p 1 for black, -1 for white
	 */
	public void setPlayer(int p) { player = p; }
	
	/**
	 * Checks whether this SGFNode was created from the SGF file or not.
	 * @return true if this SGFNode was in the file
	 */
	public boolean isFromFile() { return fromFile; }
	
	/**
	 * Returns the parent of this SGFNode. 
	 * @return the parent
	 */
    public SGFNode getParent() { return parent; }
    
    /**
     * Returns the first child of this SGFNode.
     * @return the first child, or null if there are no children
     */
    public SGFNode getFirstChild() {
    	if(ch.isEmpty())
    		return null;
    	return ch.get(0);
    }
    
    /**
     * Sets the parent of this SGFNode.
     * @param parent the parent
     */
    public void setParent(SGFNode parent) { this.parent = parent; }
    
    /**
     * Returns the comment at this SGFNode.
     * @return the comment
     */
    public String getComment() { return comment; }
    
    /**
     * Sets the comment at this SGFNode.
     * @param c the comment
     */
    public void setComment(String c) { comment = c; }
    
    /**
     * Returns the size of the Board at this SGFNode.
     * @return the size
     */
    public int getSize() { return size; }
}
